Multigrid: skip re-evaluation of unrefined nodes - #5288
Conversation
fac303d to
af6c7df
Compare
af6c7df to
1a70084
Compare
| # An adaptive refinement leaves most of the mesh alone, and the nodes | ||
| # it preserves are copied rather than evaluated. | ||
| node_subset = utils.transfer_node_subset(Vc, Vf) | ||
| op2.par_loop(kernel, node_subset, *kernel_args) | ||
| utils.prolong_preserved_nodes(coarse, fine) |
There was a problem hiding this comment.
This is the main part of the PR
646da29 to
5625a7c
Compare
| # cell's count. op2.Map cannot hold the -1 that marks a slot as empty, | ||
| # and the macro-cell kernel reading through this map integrates over | ||
| # every slot alike. Point all of an empty slot's nodes at a single one |
There was a problem hiding this comment.
I'm not sure how accurate this is.
There was a problem hiding this comment.
It is certainly confusing.
macro-cell kernel
Huh?
empty slot's nodes
unclear
There was a problem hiding this comment.
Context: DG injection integrates on a macro-cell formed by all fine children, which for an adaptive mesh are not uniform, so we need a ragged array with empty slots
There was a problem hiding this comment.
I am asking about op2.Map not being able to hold a negative value
| op2.par_loop(kernel, fine.node_set, *kernel_args) | ||
| # An adaptive refinement leaves most of the mesh alone, and the nodes | ||
| # it preserves are copied rather than evaluated. | ||
| node_subset = utils.transfer_node_subset(Vc, Vf) |
There was a problem hiding this comment.
| node_subset = utils.transfer_node_subset(Vc, Vf) | |
| changed_node_subset = utils.transfer_node_subset(Vc, Vf) |
clearer?
| # of the cell's own children's nodes: the slot then describes a | ||
| # degenerate cell sitting on the patch it belongs to, whose Jacobian | ||
| # determinant, and so whose contribution, vanishes. | ||
| if not valid.all(): |
There was a problem hiding this comment.
I've just had to fix this in pyop3. Please remove this branch because it causes rank divergence. All ranks should just do it
There was a problem hiding this comment.
What do you mean? Have you fixed this bug or a different one?
| # cell's count. op2.Map cannot hold the -1 that marks a slot as empty, | ||
| # and the macro-cell kernel reading through this map integrates over | ||
| # every slot alike. Point all of an empty slot's nodes at a single one |
There was a problem hiding this comment.
It is certainly confusing.
macro-cell kernel
Huh?
empty slot's nodes
unclear
| if Vc.extruded or Vf.extruded: | ||
| # The plex of an extruded mesh is the flat base one, whose points | ||
| # carry a whole column of nodes that the Sections cannot tell apart. | ||
| return None |
There was a problem hiding this comment.
Why can't we do extruded?
| kernel accumulated from the remaining fine nodes. | ||
|
|
||
| """ | ||
| from firedrake.halo import _get_mtype |
|
|
||
|
|
||
| def restrict_preserved_nodes(fine_dual, coarse_dual): | ||
| """Add what the nodes an adaptive refinement preserved contribute to the coarse dual. |
There was a problem hiding this comment.
I don't understand why we're adding instead of copying
There was a problem hiding this comment.
I think we should be able to copy because the operator is the identity on these nodes.
| CHKERR(DMPlexGetTransitiveClosure(fine_dm.dm, fine_point[child], PETSC_TRUE, | ||
| &fine_size, &fine_closure)) | ||
| # A cell with one child that the transform nonetheless changed would | ||
| # have a closure of its own shape; leave it to the transfer kernel. |
There was a problem hiding this comment.
I don't understand this
There was a problem hiding this comment.
I think the rule to detect preserved points is more subtle. A cell might be left unrefined but still changed by the transform?
Adaptive refinement only touches part of a mesh, so most fine cells are exact copies of a coarse one. Detect those copied cells via a PETSc SF over the points an adaptive refine_sbr transform preserves, and have prolong/restrict copy their nodes' values directly instead of running them through the transfer kernel, which is both cheaper and exact where evaluation would otherwise be approximate. Also pads the macro-cell coarse-to-fine node map's empty slots (where a coarse cell has fewer fine children than the busiest one in the hierarchy) with a degenerate cell of zero measure, so the map stays rectangular without the kernel double-counting real contributions.
- Drop the temporary FIAT install step now that fiat#267 is merged. - preserved_points computes nfine/ncoarse via num_owned_cells instead of taking nfine as an argument, asserting the passed-in array shape against it rather than trusting it as the source of truth.
Follow ASD-STE100: short sentences, one idea each, active voice, and the subject named up front rather than buried in a relative clause. The AGENTS.md rules this follows are in #5338. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1c2b268 to
994901b
Compare
Name the four reasons no node can be preserved in one Notes section, and let each guard stand on its own. The collective agreement moves to where the verdict is reached, so _preserved_point_sf always returns an SF. Document why restriction accumulates instead of copying. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GRZG3iuNzgXQWGiuzrjKQo
Stack
PR 2 of 4: #5337 (
fix-dg-injection-child-count) -> this PR (adaptive-multigrid) -> #5270(
mesh-redistribution) -> #5287 (assign-submesh-restricted).Description
AI-assisted (Claude Code)
An adaptive refinement leaves most of the mesh alone. Prolongation, restriction and injection
previously treated every fine node as if the whole mesh had changed: locate the coarse cell holding
it, pull it back to the coarse reference cell, and evaluate the coarse basis there. On a cell the
refinement did not touch, that reproduces the coarse dof it started from, to within roundoff — so
this PR skips the re-evaluation and copies those nodes' values directly.
Main changes
firedrake/cython/mgimpl.pyx:preserved_pointsfinds, for a coarse/fine plex pair produced byadaptive refinement, which fine points are exact copies of a coarse point. It compares closures
together with their orientations, not just their sizes, so a cell that refinement rebuilt
identically but reoriented is not mistaken for a preserved one.
firedrake/mg/utils.py:preserved_node_sfbuilds thePETSc.SFpairing the coarse and fine nodesthat correspond to those preserved points (via
Section.distributeSection/createSectionSF,trimmed to owned fine nodes only);
transfer_node_subsetgives the complementarySubsetof finenodes the transfer kernels must still visit;
prolong_preserved_nodes/restrict_preserved_nodesdo the copy/reduce for the nodes the SF accounts for.
firedrake/mg/interface.py:prolong/restrictrun theirop2.par_looponly overtransfer_node_subset's nodes, then call the preserved-node copy/reduce for the rest.preserved_node_sfreturnsNonewhen no node can be preserved. Its docstring names the fourreasons — mismatched node layouts, an extruded mesh, meshes that are not consecutive levels, and a
uniform refinement — and states that every rank reaches the same verdict, so that a caller may
branch on it collectively. Restriction adds rather than copies, at every stage: a coarse basis
function does not vanish on the neighbouring cells that refinement did split, so a preserved coarse
node also collects a contribution from the kernel.
Tests
tests/firedrake/multigrid/test_adaptive_multigrid.py. Four transfer tests (test_DG0,test_CG1,test_restrict_CG1,test_restrict_DG0) fold into one parametrisedtest_transfers, which adds CG2and CG3 and asserts that nodes really are copied rather than evaluated (
_copied_nodes). It runsacross
firedrake/netgenmeshes,square/cube, at 1, 2, 3 and 4 processes.test_grid_transfer.pyis unchanged and still passes, which is what covers the uniform hierarchiesthis PR must leave alone.